﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using Postnuclear.Collections.Adapters;
using Postnuclear.Dependency;

namespace Postnuclear.Collections
  {
  /// <summary>
  /// Represents an observable doubly linked list.
  /// </summary>
  /// <typeparam name="T">Specifies the element type of the linked list.</typeparam>
  public sealed partial class ObservableLinkedList<T>
    : IObservableCollection<T>
    {
    #region Public

    // Constructors

    /// <summary>
    /// Initializes a new instance of the <see cref="ObservableLinkedList{T}"/> class.
    /// </summary>
    /// <param name="values">The initial values.</param>
    public ObservableLinkedList(IEnumerable<T> values)
      {
      Contract.Requires<ArgumentNullException>(values != null);

      Adapter = new NotifyCollectionChangedAdapter<T>(this);

      Expressions.Bind(() => Count, () => Last != null ? Last.Index + 1 : 0);

      values.ForEach(Add);
      }

    /// <summary>
    /// Initializes a new instance of the <see cref="ObservableLinkedList{T}"/> class.
    /// </summary>
    /// <param name="values">The initial values.</param>
    public ObservableLinkedList(params T[] values)
      : this(values as IEnumerable<T>)
      {
      Contract.Requires<ArgumentNullException>(values != null);
      }

    // Events

    /// <summary>
    /// Occurs when collection is cleared.
    /// List of items that were cleared from collection is passed as arguments value.
    /// </summary>
    public event EventHandler<ValueEventArgs<IEnumerable<T>>> CollectionCleared;

    /// <summary>
    /// Occurs when new item is added to collection.
    /// Added item is passed as arguments value.
    /// </summary>
    public event EventHandler<ObservableItemEventArgs<T>> ItemAdded;

    /// <summary>
    /// Occurs when item is removed from collection.
    /// Removed item is passed as arguments value.
    /// </summary>
    public event EventHandler<ObservableItemEventArgs<T>> ItemRemoved;

    /// <summary>
    /// Occurs when the collection changes.
    /// </summary>
    public event NotifyCollectionChangedEventHandler CollectionChanged
      {
      add
        {
        Adapter.CollectionChanged += value;
        }

      remove
        {
        Adapter.CollectionChanged -= value;
        }
      }

    /// <summary>
    /// Occurs when a property value changes.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Occurs when the <see cref="First"/> changes.
    /// </summary>
    public event EventHandler<EventArgs> FirstChanged;

    /// <summary>
    /// Occurs when the <see cref="Last"/> changes.
    /// </summary>
    public event EventHandler<EventArgs> LastChanged;

    // Properties

    /// <summary>
    /// Gets the number of elements actually contained in the <see cref="ObservableLinkedList{T}"/>.
    /// </summary>
    /// <value>
    /// The number of elements actually contained in the <see cref="ObservableLinkedList{T}"/>.
    /// </value>
    public int Count
      {
      get
        {
        return _count;
        }

      private set
        {
        Contract.Requires<ArgumentOutOfRangeException>(value >= 0);

        if (value == _count)
          {
          return;
          }

        _count = value;
        PropertyChanged.Raise(this, "Count");
        }
      }

    /// <summary>
    /// Gets a value indicating whether the <see cref="ObservableLinkedList{T}"/> is read-only.
    /// </summary>
    /// <value>
    /// <c>true</c> if the <see cref="ObservableLinkedList{T}"/> is read-only; otherwise, <c>false</c>.
    /// </value>
    public bool IsReadOnly
      {
      get
        {
        return false;
        }
      }

    /// <summary>
    /// Gets the first node of the <see cref="ObservableLinkedList{T}"/>.
    /// </summary>
    public Node First
      {
      get
        {
        return _first;
        }

      private set
        {
        if (value == _first)
          {
          return;
          }

        _first = value;
        FirstChanged.Raise(this);
        PropertyChanged.Raise(this, "First");
        }
      }

    /// <summary>
    /// Gets the last node of the <see cref="ObservableLinkedList{T}"/>.
    /// </summary>
    public Node Last
      {
      get
        {
        return _last;
        }

      private set
        {
        if (value == _last)
          {
          return;
          }

        _last = value;
        LastChanged.Raise(this);
        PropertyChanged.Raise(this, "Last");
        }
      }

    // Methods

    /// <summary>
    /// Returns an enumerator that iterates through the collection.
    /// </summary>
    /// <returns>A <see cref="IEnumerator{T}"/> that can be used to iterate through the collection.</returns>
    public IEnumerator<T> GetEnumerator()
      {
      return Nodes.Select(node => node.Value).GetEnumerator();
      }

    /// <summary>
    /// Returns an enumerator that iterates through the collection.
    /// </summary>
    /// <returns>A <see cref="IEnumerator"/> that can be used to iterate through the collection.</returns>
    IEnumerator IEnumerable.GetEnumerator()
      {
      return GetEnumerator();
      }

    /// <summary>
    /// Adds the specified node. node should be configured already.
    /// </summary>
    /// <param name="item">The object to add to the <see cref="ObservableLinkedList{T}"/>.</param>
    public void Add(T item)
      {
      this.AddLast(item);
      }

    /// <summary>
    /// Adds the specified node. node should be configured already.
    /// </summary>
    /// <param name="node">The node which will be injected, Must be properly configured already.</param>
    internal void Add(Node node)
      {
      Contract.Requires<ArgumentNullException>(node != null);
      Contract.Requires<InvalidOperationException>(node.List == null);
      Contract.Requires<InvalidOperationException>(node.Previous == null || node.Previous.List == this);
      Contract.Requires<InvalidOperationException>(node.Next == null || node.Next.List == this);
      Contract.Requires<InvalidOperationException>(
        node.Next != null
        || node.Previous != null
        || (First == null && Last == null));
      Contract.Requires<InvalidOperationException>(
        node.Next == null
        || node.Previous == null
        || (node.Previous.Next == node.Next && node.Next.Previous == node.Previous));

      node.List = this;

      if (node.Previous != null)
        {
        node.Previous.Next = node;
        }
      else
        {
        First = node;
        }

      if (node.Next != null)
        {
        node.Next.Previous = node;
        }
      else
        {
        Last = node;
        }

      node.InternalValueChanged += OnNodeInternalValueChanged;
      ItemAdded.Raise(this, node.Value, node.Index);
      }

    /// <summary>
    /// Removes all items from the <see cref="ObservableLinkedList{T}"/>.
    /// </summary>
    public void Clear()
      {
      Node[] nodes = Nodes.ToArray();

      First = null;
      Last = null;

      nodes.ForEach(ResetNode);
      CollectionCleared.Raise(this, nodes.Select(node => node.Value));
      }

    /// <summary>
    /// Determines whether the <see cref="ObservableLinkedList{T}"/> contains a specific value.
    /// </summary>
    /// <param name="item">The object to locate in the <see cref="ObservableLinkedList{T}"/>.</param>
    /// <returns>
    /// <c>true</c> if item is found in the <see cref="ObservableLinkedList{T}"/>; otherwise, <c>false</c>.
    /// </returns>
    public bool Contains(T item)
      {
      return this.Find(item) != null;
      }

    /// <summary>
    /// Copies the elements of the <see cref="ObservableLinkedList{T}"/> to an <see cref="Array"/>,
    /// starting at a particular <see cref="Array"/> index.
    /// </summary>
    /// <param name="array">
    /// The one-dimensional <see cref="Array"/> that is the destination of the elements
    /// copied from <see cref="ObservableLinkedList{T}"/>. The <see cref="Array"/> must
    /// have zero-based indexing.
    /// </param>
    /// <param name="arrayIndex">The zero-based index in array at which copying begins.</param>
    public void CopyTo(T[] array, int arrayIndex)
      {
      Nodes.ForEach(node => array[arrayIndex + node.Index] = node.Value);
      }

    /// <summary>
    /// Removes the specified node from the <see cref="ObservableLinkedList{T}"/>.
    /// </summary>
    /// <param name="item">The object to remove from the <see cref="ObservableLinkedList{T}"/>.</param>
    /// <returns>
    /// <c>true</c> if item was successfully removed from the <see cref="ObservableLinkedList{T}"/>;otherwise, <c>false</c>.
    /// This method also returns <c>false</c> if item is not found in the original <see cref="ObservableLinkedList{T}"/>.
    /// </returns>
    public bool Remove(T item)
      {
      Node node = this.Find(item);
      return node != null && Remove(node);
      }

    /// <summary>
    /// Removes the specified node from the <see cref="ObservableLinkedList{T}"/>.
    /// </summary>
    /// <param name="node">The <see cref="Node"/> to remove from the <see cref="ObservableLinkedList{T}"/>.</param>
    /// <returns>
    /// <c>true</c> if item was successfully removed from the <see cref="ObservableLinkedList{T}"/>;otherwise, <c>false</c>.
    /// This method also returns <c>false</c> if item is not found in the original <see cref="ObservableLinkedList{T}"/>.
    /// </returns>
    public bool Remove(Node node)
      {
      Contract.Requires<ArgumentNullException>(node != null);

      Contract.Ensures(node.List == null);
      Contract.Ensures(node.Next == null);
      Contract.Ensures(node.Previous == null);

      if (node.List != this)
        {
        return false;
        }

      int index = node.Index;

      if (node.Next != null)
        {
        node.Next.Previous = node.Previous;
        }
      else
        {
        Last = node.Previous;
        }

      if (node.Previous != null)
        {
        node.Previous.Next = node.Next;
        }
      else
        {
        First = node.Next;
        }

      ResetNode(node);
      ItemRemoved.Raise(this, node.Value, index);

      return true;
      }

    /// <summary>
    /// Gets the list nodes sequence.
    /// </summary>
    public IEnumerable<Node> Nodes
      {
      get
        {
        Node node = First;

        while (node != null)
          {
          yield return node;
          node = node.Next;
          }
        }
      }

    #endregion

    #region Private

    // Methods

    private void ResetNode(Node node)
      {
      node.Next = null;
      node.Previous = null;
      node.List = null;
      node.InternalValueChanged -= OnNodeInternalValueChanged;
      }

    private void OnNodeInternalValueChanged(object sender, ValueEventArgs<KeyValuePair<T, T>> e)
      {
      Node node = (Node)sender;
      ItemRemoved.Raise(this, e.Value.Key, node.Index);

      ItemRemoved.Raise(this, e.Value.Value, node.Index);
      }

    [ContractInvariantMethod]
    private void Validate()
      {
      Contract.Invariant((First == null && Last == null && Count == 0) || (First != null && Last != null && Count > 0));
      Contract.Invariant(Contract.ForAll(Nodes, node => node.List == this));

      Contract.Invariant(First == null || (First.Previous == null && First.Index == 0));
      Contract.Invariant(Contract.ForAll(Nodes, node => node == First || (node.Previous.Next == node)));

      Contract.Invariant(Last == null || (Last.Next == null && Last.Index == Count - 1));
      Contract.Invariant(Contract.ForAll(Nodes, node => node == Last || (node.Next.Previous == node)));
      }

    // Fields

    private readonly INotifyCollectionChanged Adapter;

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private Node _first;

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private Node _last;

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private int _count;

    #endregion
    }
  }